package fr.gla.hibtracker.historique.collection;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.hibernate.HibernateException;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.event.EventSource;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.SingleTableEntityPersister;
import org.hibernate.sql.Insert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.gla.hibtracker.historique.bm.ConfigModification;
import fr.gla.hibtracker.historique.bm.ConfigModificationID;
import fr.gla.hibtracker.historique.bm.FlushModifications;
import fr.gla.hibtracker.historique.bm.GroupeModifications;
import fr.gla.hibtracker.historique.bm.Modification;
import fr.gla.hibtracker.historique.bm.ModificationID;
import fr.gla.hibtracker.historique.listener.FlushModificationThreadLocal;

/**
 * Class helper permettant d'historiser les modifications d'une collection
 * @author glaccruche
 *
 */
public class HistoriqueCollectionHelper {

	private Object owner;

	private String role;

	private String oldValue;

	private String newValue;

	private EventSource eventSource;

	public HistoriqueCollectionHelper(EventSource eventSource,String role,Object owner,String oldValue,String newValue) {
		this.owner = owner;
		this.eventSource = eventSource;
		this.role = role;
		this.oldValue = oldValue;
		this.newValue = newValue;
	}

	private static final Logger LOGGER = LoggerFactory.getLogger(HistoriqueCollectionHelper.class);

	/**
	 * Historise les modification d'une collection
	 */
	public void historise(){
		if(role != null){
			String className = getOwner().getClass().getName();
			String el = getRole().replaceFirst(className+"[.]", "");
			ConfigModificationID configModificationID = constructConfigModificationId(className, el);
			CriteriaImpl criteria = new CriteriaImpl(ConfigModification.class.getName(),getEventSource());
	        criteria.add(Restrictions.eq("isPersist", true));
	        criteria.add(Restrictions.eq("id.className",configModificationID.getClassName()));
	        criteria.add(Restrictions.eq("id.el", configModificationID.getEl()));
	        if(LOGGER.isDebugEnabled()) {
                LOGGER.debug("try to find confProperty "+configModificationID);
            }
	        List<ConfigModification> configModification = getEventSource().list(criteria);
	        FlushModifications flushModifications = FlushModificationThreadLocal.getFlushModifications();
	        if(!configModification.isEmpty()){
	        	if(LOGGER.isDebugEnabled()) {
                    LOGGER.debug("found config modification for "+configModificationID);
                }
	        	GroupeModifications<?> currentGroupModifications = flushModifications.getGroupeModifications(getOwner());
	        	GroupeModifications groupeModifications = null;
	        	if(currentGroupModifications != null){
	        		if(LOGGER.isDebugEnabled()) {
                        LOGGER.debug("found group modification for object : "+getOwner());
                    }
	        		groupeModifications = currentGroupModifications;

	        	}else{
	        		if(LOGGER.isDebugEnabled()) {
                        LOGGER.debug("no previous group modification for object : "+getOwner()+", create one");
                    }
	        		groupeModifications = new GroupeModifications();
	        		groupeModifications.setObject(getOwner());
	        	}

	        	Modification modification  = new Modification();

	            ModificationID modificationID = new ModificationID();
	            modificationID.setConfigModification(configModification.get(0));
	            modificationID.setGroupeModifications(groupeModifications);

	            modification.setOldValue(getOldValue());
	            modification.setNewValue(getNewValue());
	            modification.setModificationID(modificationID);
	            if(LOGGER.isDebugEnabled()){
	            	LOGGER.debug("add modification property :\n"+modification);
	            }
	            groupeModifications.addModification(modification);
	            //no previous save of flushModification
	            if(flushModifications.getGroupesModifications().isEmpty()){
	            	if(LOGGER.isDebugEnabled()){
	            		LOGGER.debug("no previous flushModification save - insert flush and group modif");
	            	}
	            	Serializable flushID = executeInsert(eventSource, flushModifications);
	                Serializable groupeModifId = executeInsert(eventSource, groupeModifications, flushID);
	                executeInsert(eventSource, modification,groupeModifId);
	            }else{
	            	if(groupeModifications == currentGroupModifications){
	            		if(LOGGER.isDebugEnabled()){
		            		LOGGER.debug("found previous GroupeModification, only insert modification");
		            	}
	            		executeInsert(eventSource, modification);
	            	}else{
	            		if(LOGGER.isDebugEnabled()){
		            		LOGGER.debug("no previous GroupeModification,  insert group modification with it's modification");
		            	}
	            		Serializable groupeModifId = executeInsert(eventSource, groupeModifications, flushModifications.getId());
		                executeInsert(eventSource, modification,groupeModifId);
	            	}


	            }
	            if(groupeModifications != currentGroupModifications) {
                    flushModifications.addGroupesModification(groupeModifications);
                }
	            FlushModificationThreadLocal.setFlushModifications(flushModifications);
	        }else{
	        	if(LOGGER.isDebugEnabled()) {
                    LOGGER.debug("could not find confProperty for "+configModificationID);
                }
	        }
		}
	}

	/**
	 * Insert une FlushModification en cr�ant une requ�te SQL
	 * @param eventSource la session hibernate
	 * @param flushModifications l'object de type FlushModification a ins�rer
	 * @return l'identifiant de la FlushModification
	 */
	public Serializable executeInsert(EventSource eventSource,FlushModifications flushModifications){
		return executeInsert(eventSource, flushModifications, createInsertFlushModification(eventSource));
	}

	/**
	 * Insert un GroupeModifications en cr�ant une requ�te SQL
	 * @param eventSource la session hibernate
	 * @param groupeModifications l'objet de type GroupeModifications
	 * @param flushID l'identifiant de la FlushModification associ�
	 * @return l'identifiant de la GroupeModifications
	 */
	public Serializable executeInsert(EventSource eventSource,GroupeModifications groupeModifications,Serializable flushID){
		return executeInsert(eventSource, groupeModifications, createInsertGroupeModification(eventSource), flushID);
	}

	/**
	 *
	 * @param eventSource
	 * @param modification
	 * @param groupeModifId
	 */
	public void executeInsert(EventSource eventSource,Modification modification, Serializable groupeModifId){
		modification.getModificationID().getGroupeModifications().setId((Integer) groupeModifId);
		executeInsert(eventSource, modification, createInsertModification(eventSource));
	}

	/**
	 * Cr�e le prepareStatement et l'execute pour ins�rer la FlushModification
	 * @param eventSource la session hibernate
	 * @param flushModifications l'object de type FlushModification a ins�r�
	 * @param insert l'objet d'insertion de la FlushModification
	 * @return l'identifiant du FlushModification ins�r�
	 */
	public Serializable executeInsert(EventSource eventSource,FlushModifications flushModifications,Insert insert){
		SessionFactoryImplementor sessionFactoryImplementor = eventSource.getFactory();
		SingleTableEntityPersister persister = (SingleTableEntityPersister) sessionFactoryImplementor.getClassMetadata(FlushModifications.class);
		PreparedStatement preparedStatement = null;
		try{
			preparedStatement = eventSource.getBatcher().prepareStatement(insert.toStatementString());
			preparedStatement.setTimestamp(1, new Timestamp(flushModifications.getDateModification().getTime()));
			preparedStatement.setString(2, flushModifications.getUser());
			Serializable identifier = persister.getIdentifierGenerator().generate(eventSource,flushModifications );
			preparedStatement.setObject(3, identifier);
			preparedStatement.executeUpdate();
			return identifier;
		}catch (SQLException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		catch (HibernateException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		finally{
			try {
				preparedStatement.close();
			} catch (SQLException e) {
				LOGGER.error(e.toString(), e);
			}
		}
		return null;
	}

	/**
	 * Cr�e le prepareStatement et l'execute pour ins�rer la GroupeModifications
	 * @param eventSource eventSource la session hibernate
	 * @param groupeModifications l'object de type GroupeModifications a ins�r�
	 * @param insert l'objet d'insertion du GroupeModifications
	 * @param flushID
	 * @return
	 */
	public Serializable executeInsert(EventSource eventSource,GroupeModifications groupeModifications,Insert insert,Serializable flushID){
		PreparedStatement preparedStatement = null;
		try{
			SessionFactoryImplementor sessionFactoryImplementor = eventSource.getFactory();
			SingleTableEntityPersister classMetadata = (SingleTableEntityPersister) findGroupeModificationsForObject(sessionFactoryImplementor);
			SingleTableEntityPersister objectPersister = (SingleTableEntityPersister) eventSource.getFactory().getClassMetadata(getOwner().getClass());
			preparedStatement = eventSource.getBatcher().prepareStatement(insert.toStatementString());
			preparedStatement.setObject(1, flushID);
			Serializable objectID = objectPersister.getIdentifier(getOwner(), eventSource.getEntityMode());
			preparedStatement.setObject(2,objectID);
			Serializable serializable = classMetadata.getIdentifierGenerator().generate(eventSource,groupeModifications);
			preparedStatement.setObject(3,serializable);
			String discriminatorSQLValue = classMetadata.getDiscriminatorSQLValue();
			discriminatorSQLValue = (discriminatorSQLValue !=null && discriminatorSQLValue.length() != 0) ? discriminatorSQLValue.replaceAll("'", "") :  discriminatorSQLValue;
			preparedStatement.setObject(4,discriminatorSQLValue);
			preparedStatement.executeUpdate();
			groupeModifications.setId((Integer)serializable);
			groupeModifications.setObject(getOwner());
			return serializable;
		}catch (SQLException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		catch (HibernateException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		finally{
			try {
				if(preparedStatement != null) {
                    preparedStatement.close();
                }
			} catch (SQLException e) {
			    LOGGER.error(e.toString(), e);
			}
		}
		return null;
	}

	public void executeInsert(EventSource eventSource,Modification modification){
		executeInsert(eventSource, modification, createInsertModification(eventSource));
	}

	public void executeInsert(EventSource eventSource,Modification modification,Insert insert){
		PreparedStatement preparedStatement = null;
		try{
			preparedStatement = eventSource.getBatcher().prepareStatement(insert.toStatementString());
			preparedStatement.setString(1, modification.getOldValue());
			preparedStatement.setString(2, modification.getNewValue());
			preparedStatement.setInt(3, modification.getModificationID().getGroupeModifications().getId());
			preparedStatement.setString(4, modification.getModificationID().getConfigModification().getId().getClassName());
			preparedStatement.setString(5, modification.getModificationID().getConfigModification().getId().getEl());
			preparedStatement.executeUpdate();
		}catch (SQLException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		catch (HibernateException e) {
			LOGGER.error("problem when trying to add historisation modification on collection",e);
		}
		finally{
			try {
				preparedStatement.close();
			} catch (SQLException e) {
			    LOGGER.error(e.toString(), e);
			}
		}
	}

	/**
	 * Recherche dans la session factory hibernate les mapping assignable a une entit� de type GroupeModification
	 * @param sessionFactoryImplementor
	 * @return mapping
	 */
	private ClassMetadata[] findGroupeModificationsAssignable(SessionFactoryImplementor sessionFactoryImplementor){
		List<ClassMetadata> list = new ArrayList<ClassMetadata>();
		Map<String, ClassMetadata> map = sessionFactoryImplementor.getAllClassMetadata();
		for(Entry<String, ClassMetadata> entry : map.entrySet()){
			try {
				if(GroupeModifications.class.isAssignableFrom(Class.forName(entry.getKey()))){
					list.add(entry.getValue());
				}
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		return list.toArray(new ClassMetadata[]{});
	}

	/**
	 * Recherche dans la session factory hibernate les mapping assignable a une entit� de type GroupeModification
	 * dont la propriete Object et assignable a l'objet Owner
	 * @param sessionFactoryImplementor
	 * @return mapping
	 */
	private ClassMetadata findGroupeModificationsForObject(SessionFactoryImplementor sessionFactoryImplementor){
		ClassMetadata[] classMetadatas = findGroupeModificationsAssignable(sessionFactoryImplementor);
		for(ClassMetadata classMetadata : classMetadatas){
			if(classMetadata.getPropertyType("object").getReturnedClass().isAssignableFrom(getOwner().getClass())){
				return classMetadata;
			}
		}
		return null;
	}

	/**
	 * Cr�e la requete d'insertion d'une entit� GroupeModification
	 * @param eventSource
	 * @returnla requete d'insertion
	 */
	public Insert createInsertGroupeModification(EventSource eventSource){
		SessionFactoryImplementor sessionFactoryImplementor = eventSource.getFactory();
		ClassMetadata classMetadata = findGroupeModificationsForObject(sessionFactoryImplementor);
		if(classMetadata != null){
			SingleTableEntityPersister persister = (SingleTableEntityPersister) classMetadata;
			Insert insert = new Insert( sessionFactoryImplementor.getDialect() )
			.setTableName( persister.getTableName() )
			.addColumns(persister.getPropertyColumnNames("flushModifications"))
			.addColumns(persister.getPropertyColumnNames("object"))
			.addColumns(persister.getIdentifierColumnNames())
			.addColumn(persister.getDiscriminatorColumnName());
			return insert;
		}else{
			LOGGER.error("Tentative d'historisation d'un groupe de modification non pris en charge");
		}

		return null;
	}

	/**
	 * Cr�e la requete d'insertion d'une entit� FlushModition
	 * @param eventSource
	 * @return la requete d'insertion
	 */
	public Insert createInsertFlushModification(EventSource eventSource){
		SessionFactoryImplementor sessionFactoryImplementor = eventSource.getFactory();
		SingleTableEntityPersister persister = (SingleTableEntityPersister) sessionFactoryImplementor.getClassMetadata(FlushModifications.class);
		Insert insert = new Insert( sessionFactoryImplementor.getDialect() )
		.setTableName( persister.getTableName() )
		.addColumns(persister.getPropertyColumnNames("dateModification"))
		.addColumns(persister.getPropertyColumnNames("user"))
		.addColumns(persister.getIdentifierColumnNames());
		return insert;
	}

	/**
	 * Cr�e la requete d'insertion d'une entit� Modification
	 * @param eventSource
	 * @return la requete d'insertion
	 */
	public Insert createInsertModification(EventSource eventSource){
		SessionFactoryImplementor sessionFactoryImplementor = eventSource.getFactory();
		SingleTableEntityPersister persister = (SingleTableEntityPersister) sessionFactoryImplementor.getClassMetadata(Modification.class);
		Insert insert = new Insert( sessionFactoryImplementor.getDialect() )
		.setTableName( persister.getTableName() )
		.addColumns(persister.getPropertyColumnNames("oldValue"))
		.addColumns(persister.getPropertyColumnNames("newValue"))
		.addColumns(persister.getIdentifierColumnNames());
		return insert;
	}


	public ConfigModificationID constructConfigModificationId(String className,String el){
		ConfigModificationID configModificationID = new ConfigModificationID();
		configModificationID.setClassName(className);
		configModificationID.setEl(el);
		return configModificationID;
	}

	public Object getOwner() {
		return owner;
	}

	public String getRole() {
		return role;
	}

	public EventSource getEventSource() {
		return eventSource;
	}

	public String getOldValue() {
		return oldValue;
	}

	public String getNewValue() {
		return newValue;
	}
}
